package com.kedacom.ctsp.authz.security.provider;

import com.kedacom.ctsp.authz.AuthenticationService;
import com.kedacom.ctsp.authz.entity.Authentication;
import com.kedacom.ctsp.authz.security.AuthzSecurityProperties;
import com.kedacom.ctsp.authz.token.TokenInformation;
import com.kedacom.ctsp.serialize.ISerialization;
import com.kedacom.ctsp.serialize.SerializationException;
import com.kedacom.ctsp.serialize.SerializationUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;

import static java.util.stream.Collectors.toList;


/**
 * Spring Security on redis
 *
 * @author xuwei
 * @create 2017-11-28 19:57
 **/
@Slf4j
public class RedisSessionRegistryImpl implements AuthzSessionRegistry {

    private static final String SESSIONIDS = "security_sessionIds_";
    private static final String PRINCIPALS = "security_principals_";

    @Autowired
    private ISerialization serialization;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private AuthenticationService authenticationService;

    @Autowired
    private AuthzSecurityProperties authzSecurityProperties;

    @Autowired
    private TokenManager tokenManager;

    public RedisSessionRegistryImpl() {
    }

    @Override
    public List<Object> getAllPrincipals() {
        return new ArrayList(redisTemplate.keys(PRINCIPALS + "*"));
    }

    @Override
    public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
        Set<String> sessionsUsedByPrincipal = this.getPrincipals(((UserDetails) principal).getUsername());
        return getAllSessions(sessionsUsedByPrincipal, includeExpiredSessions);
    }

    private List<SessionInformation> getAllSessions(Set<String> sessionIds, boolean includeExpiredSessions) {
        if (sessionIds == null) {
            return Collections.emptyList();
        } else {
            List<SessionInformation> list = new ArrayList(sessionIds.size());
            Iterator var5 = sessionIds.iterator();

            while (true) {
                SessionInformation sessionInformation;
                do {
                    do {
                        if (!var5.hasNext()) {
                            return list;
                        }

                        String sessionId = (String) var5.next();
                        sessionInformation = this.getSessionInformation(sessionId);
                    } while (sessionInformation == null);
                } while (!includeExpiredSessions && sessionInformation.isExpired());

                list.add(sessionInformation);
            }
        }
    }

    @Override
    public AuthzSessionInformation getSessionInformation(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        return this.getSessionInfo(sessionId);
    }

    @Override
    public void onApplicationEvent(SessionDestroyedEvent event) {
        String sessionId = event.getId();
        this.removeSessionInformation(sessionId);
    }

    @Override
    public void refreshLastRequest(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        SessionInformation info = this.getSessionInformation(sessionId);
        if (info != null) {
            info.refreshLastRequest();
            getSessionOperation(sessionId).expire(authzSecurityProperties.getExpireTime(), TimeUnit.MINUTES);
        }

    }

    @Override
    public void registerNewSession(String sessionId, Object principal) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        Assert.notNull(principal, "Principal required as per interface contract");
        if (log.isDebugEnabled()) {
            log.debug("Registering session " + sessionId + ", for principal " + principal);
        }

        if (this.getSessionInformation(sessionId) != null) {
            this.removeSessionInformation(sessionId);
        }
        if (StringUtils.isNotBlank(tokenManager.getCurrentToken())) {
            return;
        }
        this.addSessionInfo(sessionId, new AuthzSessionInformation(principal, sessionId, new Date()));

        Set<String> sessionsUsedByPrincipal = this.getPrincipals(principal.toString());
        if (sessionsUsedByPrincipal == null) {
            sessionsUsedByPrincipal = new CopyOnWriteArraySet();
            Set<String> prevSessionsUsedByPrincipal = (Set) this.putIfAbsentPrincipals(principal.toString(), sessionsUsedByPrincipal);
            if (prevSessionsUsedByPrincipal != null) {
                sessionsUsedByPrincipal = prevSessionsUsedByPrincipal;
            }
        }

        sessionsUsedByPrincipal.add(sessionId);
        this.putPrincipals(principal.toString(), sessionsUsedByPrincipal);
        if (log.isTraceEnabled()) {
            log.trace("Sessions used by '" + principal + "' : " + sessionsUsedByPrincipal);
        }

    }

    @Override
    public void removeSessionInformation(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        SessionInformation info = this.getSessionInformation(sessionId);
        if (info != null) {
            if (log.isTraceEnabled()) {
                log.debug("Removing session " + sessionId + " from set of registered sessions");
            }

            this.removeSessionInfo(sessionId);
            Set<String> sessionsUsedByPrincipal = (Set) this.getPrincipals(info.getPrincipal().toString());
            if (sessionsUsedByPrincipal != null) {
                if (log.isDebugEnabled()) {
                    log.debug("Removing session " + sessionId + " from principal's set of registered sessions");
                }

                sessionsUsedByPrincipal.remove(sessionId);
                if (sessionsUsedByPrincipal.isEmpty()) {
                    if (log.isDebugEnabled()) {
                        log.debug("Removing principal " + info.getPrincipal() + " from registry");
                    }

                    this.removePrincipal(((UserDetails) info.getPrincipal()).getUsername());
                }

                if (log.isTraceEnabled()) {
                    log.trace("Sessions used by '" + info.getPrincipal() + "' : " + sessionsUsedByPrincipal);
                }

            }
        }
    }

    public void addSessionInfo(final String sessionId, final AuthzSessionInformation sessionInformation) {
        BoundValueOperations<String, String> hashOperations = getSessionOperation(sessionId);
        hashOperations.set(SerializationUtil.serializeToString(serialization, sessionInformation), authzSecurityProperties.getExpireTime(), TimeUnit.MINUTES);
    }

    public AuthzSessionInformation getSessionInfo(final String sessionId) {
        BoundValueOperations<String, String> hashOperations = getSessionOperation(sessionId);
        try {
            String value = hashOperations.get();
            return SerializationUtil.deserialize(serialization, value);
        } catch (SerializationException e) {
            return null;
        }
    }

    public void removeSessionInfo(final String sessionId) {
        redisTemplate.delete(sessionId);
    }

    private BoundValueOperations<String, String> getSessionOperation(final String sessionId) {
        return redisTemplate.boundValueOps(SESSIONIDS + sessionId);
    }

    public Set<String> putIfAbsentPrincipals(final String key, final Set<String> set) {
        BoundValueOperations<String, Set<String>> hashOperations = getPrincipalOperation(key);
        hashOperations.set(set, authzSecurityProperties.getExpireTime(), TimeUnit.MINUTES);
        return hashOperations.get();
    }

    public void putPrincipals(final String key, final Set<String> set) {
        BoundValueOperations<String, Set<String>> hashOperations = getPrincipalOperation(key);
        hashOperations.set(set, authzSecurityProperties.getExpireTime(), TimeUnit.MINUTES);
    }

    public Set<String> getPrincipals(final String key) {
        BoundValueOperations<String, Set<String>> hashOperations = getPrincipalOperation(key);
        return hashOperations.get();
    }

    public Set<String> getPrincipalsKeySet(String principal) {
        BoundValueOperations<String, Set<String>> hashOperations = getPrincipalOperation(principal);
        return hashOperations.get();
    }

    private BoundValueOperations<String, Set<String>> getPrincipalOperation(String principal) {
        return redisTemplate.boundValueOps(PRINCIPALS + principal);
    }

    public void removePrincipal(final String principal) {
        redisTemplate.delete(principal);
    }


    @Override
    public AuthzSessionInformation getTokenInfo(String token) {
        return getSessionInfo(token);
    }

    @Override
    public List<TokenInformation> getAllTokens(String username) {
        Set<String> sessionsUsedByPrincipal = this.getPrincipals(username);
        return getAllSessions(sessionsUsedByPrincipal, true)
                .stream().map(TokenInformation.class::cast).collect(toList());
    }

    @Override
    public boolean userIsLoggedIn(String username) {
        Set<String> sessionsUsedByPrincipal = this.getPrincipals(username);
        return CollectionUtils.isNotEmpty(sessionsUsedByPrincipal);
    }

    @Override
    public boolean tokenIsLoggedIn(String token) {
        return null != getSessionInfo(token);
    }

    @Override
    public long totalUser() {
        return getAllPrincipals().size();
    }

    @Override
    public long totalToken() {
        return redisTemplate.keys("*").size();
    }

    @Override
    public List<Authentication> getAllLoggedUser() {
        return getAllPrincipals().stream()
                .map(String.class::cast)
                .map(authenticationService::loadUserByUsername)
                .collect(toList());
    }

    @Override
    public void signOutByUserId(String username) {
        Set<String> sessionsUsedByPrincipal = this.getPrincipals(username);
        if (CollectionUtils.isNotEmpty(sessionsUsedByPrincipal)) {
            sessionsUsedByPrincipal.stream().forEach(this::removeSessionInfo);
        }
    }

    @Override
    public void signOutByToken(String token) {
        removeSessionInfo(token);
    }


    @Override
    public void signIn(HttpServletRequest request, String token, String username) {
        this.signIn(authenticationService, request, token, username);
    }

    @Override
    public void touch(String token) {
        refreshLastRequest(token);
    }
}
